
/**
 * @author Team Oscar
 */
// NOTE:
//  the tinymce object model works well until you are using callbacks. Then you have to explicitly set the context.
//
//  i.e. setTimeout(this.doSomething, 1000) FAILS. this is set to window...
//  therefore:
//
//  var t = this;
//  setTimeout(function (){t.doSomething()}, 1000); is the correct solution.
//
(function() {

	var CONNECTION_INTERVAL = 1000;
	
	//TODO move these functions somewhere else
	// (ALSO: write a proper DOM iteration abstraction :)
	
	/**
	 * Get a textNode from the DOM.
	 *
	 * If the given node is a textNode, return it. If it has
	 * any children that are textNodes, return the first of them,
	 * otherwise return the previous_textNode of this one.
	 *
	 * Bound should be set to a node that is the container for
	 * the bit of the DOM you are interested in. (i.e. it will
	 * not be returned, and nothing outside it will be returned,
	 * assuming the stasrting node was within it)
	 *
	 * @param {node} n  The node to start at
	 * @param {node} bound  The node to stop at
	 */
	function last_textNode(n, bound)
	{
		if (!n || n == bound) return null;

		if (n.nodeType == 3)
		{
			return n;
		}
		else if (n.childNodes)
		{
			return last_textNode(n.lastChild, bound);
		}
		else
		{
			return previous_textNode(n, bound);
		}
	}
	/**
	 * Get the previous textNode from the DOM.
	 *
	 * If the given node has a previous sibling, return the last_textNode
	 * of it, otherwise find the textNode that precedes this node's parentNode.
	 * 
	 * @param {node} n The node to start at
	 * @param {node} bound  @see last_textNode
	 */
	function previous_textNode(n, bound)
	{
		if (!n || n == bound) return null;

		if (n.previousSibling)
		{
			return last_textNode(n.previousSibling, bound);
		}
		else
		{
			return previous_textNode(n.parentNode, bound);
		}
	}

	/**
	 * Get the first textNode from the DOM.
	 *
	 * If the node passed in is a textNode, return that,
	 * otherwise if it has any child nodes that contain
	 * textNodes (at any depth) return the first of them,
	 * if not, return the next_textNode of this node.
	 *
	 * @param {node} n  The node to start at
	 * @param {node} bound @see last_textNode
	 */
	function first_textNode(n, bound)
	{
		if (!n || n == bound) return null;

		if (n.nodeType == 3)
		{
			return n;
		}
		else if (n.childNodes)
		{
			return first_textNode(n.firstChild, bound);
		}
		else
		{
			return next_textNode(n, bound);
		}
	}
	/**
	 * Get the next textNode from the DOM.
	 *
	 * Will get the first_textNode of the next sibling of the argument,
	 * or the next_textNode of the parent of the argument.
	 *
	 * @param {node} n  The node to start at
	 * @param {node} bound  @see last_textNode
	 */
	function next_textNode(n, bound)
	{
		if (!n || n == bound) return null;

		if (n.nextSibling)
		{
			return first_textNode(n.nextSibling, bound);
		}
		else
		{
			return next_textNode(n.parentNode, bound);
		}
	}
	/**
	 * Take two sorted arrays, merge them, and delete duplicated elements
	 */
	function array_merge_unique(a, b)
	{
		var i=0;
		var j=0;

		var res = [];
		while (i < a.length && j < b.length)
		{
			if (a[i] < b[j])
			{
				if (res[res.length-1] != a[i])
					res.push(a[i]);
				i++;
			}
			else
			{
				if (res[res.length-1] != b[j])
					res.push(b[j]);
				j++;
			}
		}
		return res;
	}

/* ************ PROPER CLASSES START HERE ***************** */
	/**
	 * A class to keep track of a changing document.
	 *
	 * Each time a new version of the document is passed in
	 * it will store it so that it can create a diff against
	 * it when the next version is passed in.
	 *
	 * NOTE: might be cleaner to hide the ed inside here,
	 * so that gets and sets are in sync.
	 */
	tinymce.create('tinymce.concurrency.DocumentHistory', {

		/**
		 * Create a new DocumentHistory.
		 *
		 * @param {string} the first version of the document.
		 */
		DocumentHistory: function (firstVersion)
		{
			this.lastVersion = firstVersion;
		},

		/**
		 * Set the current version of the document.
		 *
		 * @param {string} the authoritative version of the document.
		 */
		set: function (nextVersion)
		{
			this.lastVersion = nextVersion;
		},

		/**
		 * Give a new version of the document, and get any changes between
		 * it and the previous version
		 *
		 * @param {string} the current version of the document.
		 * @return {diff} the difference since last time
		 */
		get: function (nextVersion)
		{
			var diff = Diff.stableDiff(this.lastVersion, nextVersion);
			this.lastVersion = nextVersion;
			return diff;
		}

	});

	/**
	 * A class that displays messages to the user.
	 *
	 * It does so by inserting rows into the table that TinyMCE uses for
	 * the document and the buttons. It is highly likely that this will
	 * break if TinyMCE changes this way of doing things.
	 *
	 * NOTE: might also be nicer to use tinymce.DOM,
	 *  but it has a very unclean interface, and really
	 *  doesn't save much writing.
	 */
	tinymce.create('tinymce.concurrency.Notifier', {
		/**
		 * Create a new notifier that will interact with the given editor
		 *
		 * @param {tinymce.Editor} ed
		 */
		Notifier: function (ed)
	   	{
			this.ed = ed;	
		},

		/**
		 * Display a message of importance to the user
		 *
		 * At the moment this is done by displaying a yellow bar at the top
		 * of the editor for 10 seconds. Though this is subject to change.
		 *
		 * If you want to display HTML messages, pass in a DOM fragment
		 * otherwise it will be assumed you wish to display a string.
		 *
		 * @param {string|Node} mesg
		 */
		warn: function (mesg)
		{
			mesg = this._get_node(mesg);
			//Create a yellow box
			var messageNode = document.createElement('td');
			messageNode.style.cssText = "background-color: #FFFFCC; border-top: 1px solid #CCCCCC; font-size: 80%; height:2em;";

			//Put a message in it
			var message = document.createElement('div');
			message.style.cssText =  "margin: 0.5em; float: left;";
			messageNode.appendChild(message).appendChild(mesg);

			//Find the row before the TinyMCE iframe
			var insertPoint = this._get_editor_row();

			//Insert the message into a row
			insertPoint.parentNode.insertBefore(document.createElement('tr'), insertPoint).appendChild(messageNode);

			//Called after 10s or when the X is clicked
			var hide = function () {
				//prevent the timer from trying to delete a deleted node
				window.clearTimeout(messageNode.hideTimeout);
				messageNode.parentNode.parentNode.removeChild(messageNode.parentNode);
			}

			//Start the clock
			messageNode.hideTimeout = window.setTimeout(hide, 10000);

			//Add a close button
			var closeButton = document.createElement('div');
			closeButton.style.cssText =  "margin: 0.5em; float: right;font-weight: bold; color: #C88";
			tinymce.dom.Event.add(closeButton, 'click', hide);
			messageNode.appendChild(closeButton).appendChild(document.createTextNode("X"));
		},

		/**
		 * Display a message that the user could ignore completely,
		 * but that which you think it'd be nice for them to know.
		 *
		 * Currently this is used for the "also editing" bar; which it
		 * displays at the bottom of the text area in blue.
		 *
		 * If you wish to display HTML, pass in a DOM fragment
				 * otherwise it will be assumed you wish to display a string. A
				 * null message will cause the bar to be removed.
		 *
		 * @param {null|string|Node} mesg
		 */
		inform: function (mesg)
		{
			mesg = this._get_node(mesg);
			if (mesg == null)
			{
				if (this.informNode != null)
				{
					this.informNode.parentNode.parentNode.removeChild(this.informNode.parentNode);
					this.informNode == null;
				}
				return;
			}
			if (this.informNode == null)
			{
				//A nice blue cell at the bottom
				this.informNode = document.createElement('td');
				this.informNode.style.cssText = "background-color: #CCCCFF; border-bottom: 1px solid #AAAACC; font-size: 80%;";

				//Insert the information node at the bottom of the editing iframe
				var insertPoint = this._get_editor_row();
				if (insertPoint.nextSibling)
					insertPoint.parentNode.insertBefore(document.createElement('tr'), insertPoint.nextSibling).appendChild(this.informNode);
				else
					insertPoint.parentNode.appendChild(document.createElement('tr')).appendChild(this.informNode);
			}

			//remove old messages
			while (this.informNode.firstChild)
			{
				this.informNode.removeChild(this.informNode.firstChild);
			}

			//Display new message
			this.informNode.appendChild(mesg);
		},

		/**
		 * Convert an input mesg into a DOM node.
		 *
		 * Used by warn and inform to automatically create TextNodes
		 * when a string is passed
		 *
		 * @param {string|Node} mesg
		 * @return {Node}
		 */
		_get_node: function (mesg)
		{
			if (mesg == null)
				return null;
			if (typeof(mesg) == 'object' && mesg.nodeName != null)
				return mesg;
			return document.createTextNode(mesg);
		},

		/**
		 * Get the row in the editor table that contains the textarea.
		 * 
		 * @error May throw an exception if it cannot find the textarea.
		 * @return {Node}
		 */ 
		_get_editor_row: function ()
		{
			var rows = this.ed.getContainer().getElementsByTagName('tr');
			for (var i=0; i<rows.length; i++)
			{
				if (rows[i].getElementsByTagName('iframe').length > 0)
					return rows[i];
			}

			throw("The concurrency plugin does not work with your version of TinyMCE");
		}
	});

	/**
	 * A dummy handler, useful for when we don't want errors to display (such as when we are integrating other's changes)
	 */
	tinymce.create('tinymce.concurrency.ErrorIgnorer', {
		diffError: function () {},
		changeError: function () {},
		blockError: function () {}
	});

	/**
	 * Displays relevant patch errors to the user.
	 */
	tinymce.create('tinymce.concurrency.ErrorHandler', {
		/*
		 * create a new ErrorHandler that can contact the user 
		 * with the given notifier.
		 *
		 * @param {tinymce.concurrency.Notifier}
		 */
		ErrorHandler: function (notifier)
		{
			this.notifier = notifier;
		},

		/**
		 * Handle the case when the entire diff is broken
		 *
		 * @param {diff}
		 */
		diffError: function (diff)
		{
			this.notifier.warn("Your recent changes conflicted with edits made by another user.");
		},

		/**
		 * And the case when one block of the diff is broken.
		 *
		 * (I've not yet managed to get this to trigger!)
		 *
		 * @param {block}
		 */
		blockError: function (block)
		{
			this.notifier.warn("Some of your recent changes conflicted with edits made by another user.");
		},

		/**
		 * Inform the user if one change could not be applied.
		 *
		 * @param {change}
		 */
		changeError: function (change)
		{
			var deleted = change.as + "..." + change.be;
			var inserted = change.i;
			if (change.e - change.s <= (change.as.length + change.be.length))
			{
				if (change.e - change.s <= change.as.length)
					deleted = change.as.substr(0, change.e - change.s);
				else
					deleted = change.as + change.be.substr(change.e - change.s - change.as.length);
			}
			if (deleted == '')
			{
				this.notifier.warn("Could not insert '"+this._makeReadable(inserted)+"' as it conflicts with other edits.");
			}
			else if (inserted == '')
			{
				this.notifier.warn("Could not delete '"+this._makeReadable(deleted)+"' as it conflicts with other edits.");
			}
			else
			{
				deleted = this._makeReadable(change.bs+deleted+change.ae);
				inserted = this._makeReadable(change.bs+inserted+change.ae);

				this.notifier.warn("Could not change '"+deleted+"' into '"+inserted+"' as it conflicts with other editors.");
			}

		},
		/**
		 * Change a string of HTML into something easier for a human to read.
		 *
		 * TODO: describe changes to HTML better
		 *
		 * @param {string} str
		 * @return {string}
		 */
		_makeReadable: function (str)
		{
			return str.replace(/^[^>]*>/,'').replace(/<[^>]*>/g,'').replace(/<[^>]*$/,'');
		}

	});

	/**
	 * A holding pen for changes that have been sent to the network, but have not yet been applied to the master document.
	 * This is so that locally applied changes remain fixed.
	 */
	tinymce.create('tinymce.concurrency.DiffQueue', {
				
		/**
		 * Create a new DiffQueue
		 */
		DiffQueue: function ()
		{
			this.queue = [];
		},

		/**
		 * Add a diff to the queue
		 * @param {diff} diff
		 */
		append: function (diff)
		{
			this.queue.unshift(diff);
		},
	
		/**
		 * Remove the first diff from the queue
		 * @param {diff} diff
		 */
		remove: function (diff)
		{
			var odiff = this.queue.pop();
			if (! Diff.isEqual(diff, odiff))
			{
				// FIXME please
				this.queue = [];
			}
		},
	
		/**
		 * Get all the diffs in the queue, in the order they were added.
		 */
		all: function ()
		{
			return this.queue;
		}

	});

	/**
	 * A connection to the concurrency server
	 */
	tinymce.create('tinymce.concurrency.Connection', {

		/**
		 * Create a new connection.
		 *
		 * @param {string} url
		 * @param {function<object>} on_receive
		 * @param {function} on_error
		 */
		Connection: function (url,on_receive, on_error)
		{
			this.on_receive = on_receive;
			this.on_error = on_error;
			this.url = url;
			this.base_url = url;
			this.queue = [];
			var t = this;
			this.ticker = window.setInterval(function() { t._on_tick() }, CONNECTION_INTERVAL)
			window.ticker = this.ticker;
		},

		/**
		 * Send a message to this connection.
		 * @param {object} msg
		 */
		send: function (msg)
		{
			this.queue.push(msg);
		},

		/**
		 * Poll the server every second.
		 */
		_on_tick: function ()
		{
			tinymce.util.XHR.send({
				url: this.url,
				type: "POST",
				data: tinymce.util.JSON.serialize(this.queue),
				scope: this,
				error: this.on_error, //set by constructor
				success: this._on_success
			});

			this.queue = [];
		},

		/**
		 * What to do when we recieve data.
		 *
		 * FIXME the way we are amalgamating diffs here will TOTALLY ruin
		 * the assumption that all messages are recieved in the same order
		 * everywhere. This BREAKS sync and presync messages. I can't think
		 * of a solution now.
		 */
		_on_success: function (data, req, o)
		{
			var msgs = tinymce.util.JSON.parse(data);
			var diffs = [];
			for (var i=0; i< msgs.length; i++)
			{
				//This is slightly hacky, we're essentially munging the two layers together
				// by sharing the id for both.

				if (msgs[i].type == "welcome")
				{
					this.url = this.base_url + "/" + msgs[i].id;
				}

				if (msgs[i].type == "diff")
				{
					diffs.push(msgs[i]);
				}
				else
				{
					try{
						this.on_receive(msgs[i]);
					}catch(e){ //It is vital (in production) that one message cannot cause the failure of the next
						throw e;
					}
				}
			}
			if (diffs.length)
			{
				this.on_receive({type: "diffs", diffs: diffs});
			}
		}	
	});

	/**
	 * Deal with the cursor.
	 *
	 * FIXME only works in Firefox
	 */
	tinymce.create('tinymce.concurrency.SelectionManager', {

		/**
		 * Connect to the given editor
		 */
		SelectionManager: function (ed)
		{
			this.ed = ed;
		},

		/**
		 * Get an object containing enough information that the cursor can
		 * be repositioned after a set of diffs have ben applied.
		 *
		 * Don't try to do clever stuff with this obj outwith this class,
		 * it will probably depend strongly on which brower you are running
		 * in.
		 *
		 * @return {obj} An object to be passed to set.
		 */
		get: function ()
		{
			var bm = this.ed.selection.getBookmark();
			if (bm.start != null)
			{
				this.previous_bm = bm;
			}
			else if (this.previous_bm)
			{
				bm = this.previous_bm;
			}
			var obj =  {
				bm: bm
				//This used to be 80% right... keeping in a comment for fun.
			//	bs: text.substr(bm.start > 10 ? bm.start - 10 : 0, bm.start > 10 ? 10 : bm.start),
			//	ae: text.substr(bm.end, 10)
			}

			//Get the context of the cursor from the textNode it is current within.
			// FIXME - when the cursor is near the start of the node, the amount of context given is too short.
			// this also needs to be fixed on the other end though
			var rng = this.ed.selection.getRng();
			if (rng.startContainer && rng.startContainer.nodeValue)
			{
				obj.bs = this._get_context(rng.startContainer, rng.startOffset); 
				obj.ae = rng.endContainer.nodeValue.substr(rng.endOffset, 10);
			}
			else
			{
				var text = this.ed.dom.getRoot().textContent;
				obj.bs = text.substr(bm.start > 10 ? bm.start - 10 : 0, bm.start > 10 ? 10 : bm.start),
				obj.ae = text.substr(bm.end, 10)
			}

			return obj;
		},

		/**
		 * Get the preceding len or 10 characters from the cursor position.
		 *
		 * Will jump back a few nodes to fill in enough context.
		 *
		 * @param {node} node  The node to start in
		 * @param {number} offset  The offset to start from (and go backwards)
		 * @param {number} len=10  The length of context to create
		 */
		_get_context: function (node, offset, len)
		{
			len = len || 10;
			var value = node.nodeValue || "";
			var context = value.substr(offset > len ? offset - len : 0, offset > len ? len : offset);
			var remainder = len - context.length;

			while (node && remainder > 0)
			{
				node = previous_textNode(node, this.ed.dom.getRoot());
				if (node)
				{
					//Why is an empty textNode "null" instead of an empty string...
					value = node.nodeValue || "";
					context = value.substr(value.length > remainder ? value.length - remainder : 0, value.length > remainder ? remainder : value.length) + context;
					remainder = len - context.length;
				}
			}
			return context;
		},

		/**
		 * Put the cursor back from whence it came.
		 *
		 * @param {obj} obj  The return value of this.get
		 */
		set: function (obj)
		{
			//Don't force the cursor to jump between textareas if you have more than one to a page.
			if (tinyMCE.activeEditor != this.ed)
				return;

			var text = this.ed.dom.getRoot().textContent || "";

			var orig = [0, obj.bm.start, obj.bm.end, text.length];

			var start_poss;

			var insert = null;

			var oldbs = obj.bs;

			//Find the possible positions for the cursor in the new text.
			// (Unfortunately these may not correspond to the positions that we have to set the cursor
			//   but they are close enough)
			if (obj.bs == "")
			{
				//If there was previously nothing before the cursor, it must stay at the start of the string.
				start_poss = [0]
			}
			else
			{
				//If we couldn't find any, try again with trailing space removed.
				// FIXME: In rare cases, when the text can be found elsewhere in the document where the trailing
				//  space is still present, the cursor will jump there instead of here.
				var len = obj.bs.length;
				obj.bs = obj.bs.replace(/[\s\r\n\t\u2000]*$/, "");

				start_poss = Diff.positionList(obj.bs, text, obj.bs.length);
				insert = "\u2000\u2000\u2000\u2000\u2000".substr(0, len - obj.bs.length);

				//Ok, the text under the cursor has been changed, just put the cursor back
				// exactly where it was and hope for the best.
				if (start_poss.length == 0)
				{
					start_poss = [obj.bm.start];
				}
			}

			//Find the end position
			var end_poss;
			//If we were at the end, stay at the end.
			//FIXME: This may fail if there are ten consecutive spaces somewhere in the document
			// (depending on which browser, and which phase of the moon)
			if (obj.ae.replace(/[\s\r\n\t\u2000]*$/, "") == "")
			{
				end_poss = [text.length];
			}
			else
			{
				end_poss = Diff.positionList(obj.ae, text, 0);

				//Again, if we couldn't find where it should be - put it back where it was.
				if (end_poss.length == 0)
					end_poss = [obj.bm.end];
			}

			//If the cursor is not highlighting a selection, ensure that it doesn't suddenly
			//select stuff
			if (obj.bm.start == obj.bm.end)
			{
				start_poss = array_merge_unique(start_poss, end_poss);
				end_poss = start_poss;
			}

			//Use the clever algorithms from the diff library to guess where the cursor has gone
			// (in case we found more than one occurance of the context)
			// This is particularly unoptimal if someone has just lent on the keyboard producing lines
			// and lines of the same character - but hey, it works.
			var poss = [[0], start_poss, end_poss, [text.length]];
			var resu = Diff.optimalPositions(orig, poss, text.length, text.length * 0.005);

			//Although we searched for the before start context without any trailing whitespace,
			//if the trailing whitespace was not stripped, do not re-insert it.
			if (text.substr(resu[1] - obj.bs.length, oldbs.length) == oldbs)
			{
				obj.bs = oldbs;
				insert = null;
			}

			//If we found the places from the context, move back there. If we didn't, we'll replace
			//the cursor where we found it.
			if (resu != null)
			{
				obj.bm.start = resu[1];
				obj.bm.end = resu[2];
			}
			
			//TODO most of the stuff from here-on-in exists to work around bugs in various browsers.
			//They are particularly bad at stripping trailing whitespace from lines - there is an option 
			//in TinyMCE to turn off this behaviour, but it causes the output that it gives to vary between
			//browsers - which leads to positive feedback of diffs - which is BAD
			
			var sel = this.ed.selection.getSel();
			var rng = sel.getRangeAt(0);

			//SO, if the cursor position is past the end of the text (happens only occasionally now that it tries to
			//find context first) then move the cursor to the end of the text, before inserting some missing whitespace,
			// (a maximum of five chars worth) and then moving the cursor to the otherside of that.
			if (obj.bm.start && text && obj.bm.start >= text.length)// && text.charAt(text.length-1) != " ")
			{
				//This is to get round firefox ignoring spaces.
				//It doesn't work in Opera unfortunately - which is obviously too clever to be fooled by such a trick.
				// U+2000 EN SPACE (a space slightly wider than normal) U+00A0 NON BREAKING SPACE does not work.
				insert = (insert ? insert : '') + "\u2000\u2000\u2000\u2000\u2000".substr(0, obj.bm.start - text.length);

				var pos = last_textNode(this.ed.dom.getRoot());
				pos.nodeValue += insert;
				rng.setStart(pos, pos.length);
				rng.setEnd(pos, pos.length);
				return;
			}

			//Right, now our selection is within range, when we select it we shouldn't jump back to the start of the document
			//(though we still seem to occasionally... *sigh)
			this.ed.selection.moveToBookmark(obj.bm);

			//NEXT, because, for unknown reasons, the .textContent property is only loosely related to the cursor position
			// [doesn't make sense to me either] so, let's check backwards and forwards a bit to see if we can find the correct cursor
			// position. (From observation, it tends to be about two characters too far forwards - irregardless of how far through the
			//  document it is)

			sel = this.ed.selection.getSel();
			rng = sel.getRangeAt(0);

			var pre_cursor = this._get_context(rng.startContainer, rng.startOffset, 12);
			if ( (pre_cursor.length >= obj.bs.length && pre_cursor.indexOf(obj.bs) + obj.bs.length != rng.startOffset) ||
				 (pre_cursor.length < obj.bs.length && obj.bs.indexOf(pre_cursor) + pre_cursor.length != obj.bs.length) )
			{

				var bound = this.ed.dom.getRoot();
				var first = rng.startContainer;
				var last = rng.startContainer;
				//TODO generalise this to deal with selections as well.
				//TODO make this more robust
				//TODO make this work in non-firefox
				//TODO make this work when context is split by a node boundary
				insert = insert || "";
				for (var i=0; i<5; i++)
				{

					var fpos = first && first.nodeValue ? first.nodeValue.lastIndexOf(obj.bs) : -1;
					var lpos = last && last.nodeValue ? last.nodeValue.indexOf(obj.bs) : -1;

					if (first && fpos >= 0)
					{                                                                                   
						first.nodeValue = first.nodeValue.substr(0, fpos + obj.bs.length) + insert + first.nodeValue.substr(fpos + obj.bs.length); 
						rng.setStart(first, fpos + obj.bs.length + insert.length);
						rng.setEnd(first, fpos + obj.bs.length + insert.length);
                                                console.log("one");
						return;
					}
					else if (first && first.nodeValue && obj.bs.indexOf(first.ndoeValue) >= 0 && obj.bs.indexOf(first.nodeValue) + first.nodeValue.length >= obj.bs.replace(/[\s\r\n\t\u2000]*$/,'').length)
					{
						first.nodeValue += insert;
						rng.setStart(first, first.length);
						rng.setEnd(first, first.length);
                                                console.log("two");
						return;
					}
					else if (last && lpos >= 0)
					{
						last.nodeValue = last.nodeValue.substr(0, lpos) + insert + last.nodeValue.substr(lpos);
						rng.setStart(last, lpos + obj.bs.length + insert.length);
						rng.setEnd(last, lpos + obj.bs.length + insert.length);
                                                console.log("three");
						return;
					}
					else if (last && last.nodeValue && obj.bs.indexOf(last.nodeValue) >= 0 && obj.bs.indexOf(last.nodeValue) + last.length == obj.bs.length)
					{
						
						last.nodeValue += insert;
						rng.setStart(last, last.length);
						rng.setEnd(last, last.length);
						console.log("four");
						return;
					}
					
					first = previous_textNode(first, bound);
					last = next_textNode(last, bound);
				}

				//The above seems to sort out any edge cases when the cursor should end up near the end of a node,
				//now we need to deal with issues that arise when moving the cursor to a position at the start of a
				//node (without messing up any other cases)
				if (pre_cursor.indexOf(obj.bs) > -1 && rng.startContainer.nodeValue.indexOf(obj.bs) == -1 &&!insert)
				{
					var offset = pre_cursor.length - (pre_cursor.lastIndexOf(obj.bs) + obj.bs.length);
					var node = rng.startContainer;

					if (offset <= rng.startOffset)
					{
						rng.setStart(rng.startContainer, rng.startOffset - offset);
						rng.setEnd(rng.startContainer, rng.startOffset);
					}
					else
					{
						//FIXME
					}
				}
				return;
			}	

			//If we have to insert some trailing whitespace, now is the time to do it.
			if (insert)
			{
				var sel = this.ed.selection.getSel();
				var rng = sel.getRangeAt(0);
				var ins = document.createTextNode(insert);
                                var pos = last_textNode(this.ed.dom.getRoot());
                                if(pos) {

                                    pos.nodeValue = pos.nodeValue + insert;
                                    rng.setStart(pos, pos.length);
                                    rng.setEnd(pos, pos.length);
                                }
			}
		},
	});

	/**
	 * A class that keeps track of the 'Master copy' of the document.
	 * This copy should be the same in all browsers that are connected.
	 */
	tinymce.create('tinymce.concurrency.MasterDocument', {
		/**
		 * Create a new master document.
		 *
		 * @param {string} doc  The current content of the document.
		 */
		MasterDocument: function (doc)
		{
			this.doc = doc;
		},

		/**
		 * Apply a diff to the master document.
		 *
		 * @param {diff} diff
		 * @param {ErrorHandler|ErrorIgnorer} handler
		 * @return {string}  The new copy of the document
		 */
		apply_diff: function (diff, handler)
		{
			this.old_doc = this.doc;
			this.doc = Diff.stablePatch(diff, this.old_doc, handler);
			return this.doc;
		},

		/**
		 * Override the content of the master document.
		 *
		 * @param {string} doc  The new document
		 */
		set: function (doc)
		{
			this.old_doc = this.doc;
			this.doc = doc;
		}

	});

	/**
	 * The main logic.
	 */
	tinymce.create('tinymce.concurrency.DiffSystem', {

		/**
		 * Set up concurrency on the given editor
		 *
		 * takes parameters from the editor
		 *
		 *  {string} concurrency_server
		 *  {string} concurrency_document_id
		 *  {string} concurrency_user_alias
		 */
		DiffSystem: function (ed)
		{
			var url = ed.getParam("concurrency_server");
			if(!url) return

			this.ed = ed;
			this.notifier = new tinymce.concurrency.Notifier(this.ed);
			this.diff_queue = new tinymce.concurrency.DiffQueue();
			this.diff_history = new tinymce.concurrency.DocumentHistory(this.getContent());
			this.master = new tinymce.concurrency.MasterDocument(this.getContent());
			var t= this;
			this.ticker = window.setInterval(function () {t.takeDiff()}, 1000 + Math.random()*2000);
			
			this.maxdiff = 0;
			this.mindiff = 0;
			this.selector = new tinymce.concurrency.SelectionManager(this.ed);
			this.connection = new tinymce.concurrency.Connection (ed.getParam("concurrency_server"), function (msg) { t.on_receive(msg)}, function (type, req, o) {t.on_error()});

			this.subscribe();

			this.sendDiff = function (diff) {
				t.connection.send({
					type: "diff",
					diff: diff
				});
			}
		},
		/**
		 * We need to subscribe at boot time, and if ever the network goes from being "down" to "up"
		 */
		subscribe: function ()
		{
			this.connection.send({
				type: "subscribe",
				user_alias: this.ed.getParam("concurrency_user_alias"), 
				document_id: this.ed.getParam("concurrency_document_id") || window.location.href
			});
			this.presync_queue = [];
		},

		/**
		 * This is called by the network when a connection error occurs.
		 * (It actually passes some parameters, but they're fairly useless)
		 */
		on_error: function ()
		{
			this.notifier.inform("The network is down");
			this.was_error = true;
		},

		/**
		 * Callback from the network when a msg is recieved.
		 *
		 * FIXME at the moment multiple diff messages are recieved necessarily out of order.
		 * To fix this, we should recieve "diff" messages directly here, and do everything
		 * except apply them to the content [which we should attach to the takeDiff timer].
		 *
		 * @param {object} msg  The recieved message
		 */
		on_receive: function (msg)
		{
			if (this.was_error)
			{
				this.subscribe();
			}

			//This is all a hack to get round the issue of recieving multiple
			//{type: diff} messages at the same time. Hidden inside the diffs message
			//is a list of diff messages
			if (msg.type == "diffs")
			{
				this.on_diffs(msg.diffs);
			}
			else if (msg.type == "welcome")
			{
				this.on_welcome(msg.id);
			}
			else if (msg.type == "users")
			{
				this.on_users(msg.users);
			}
			else if (msg.type == "presync")
			{
				this.on_presync();
			}
			else if (msg.type == "sync")
			{
				this.on_sync(msg.content);
			}
		},

		/**
		 * We recieved a diff message, 
		 *
		 * FIXME
		 */
		on_diffs: function (diffs)
		{
			var handler;

			//Force a new diff to be taken
			this.takeDiff();
			//Get the current selection
			var sel = this.selector.get();

			var newmaster;
			for (var i=0; i<diffs.length; i++)
			{
				var diff = diffs[i].diff;
				var from = diffs[i].from;
				//If we have recieved a presync message, store the diffs up
				// so we can apply them when we get a sync
				if (this.presync_queue != null)
				{
					this.presync_queue.push(diff);
				}

				//If it's from us, then we need to give error messages
				if (from == this.user_id)
				{
					handler = new tinymce.concurrency.ErrorHandler(this.notifier);
					this.diff_queue.remove(diff);
				}
				else
				{
					//Ignore error messages generated by diffs we didn't write
					handler = new tinymce.concurrency.ErrorIgnorer();
				}
				//Apply the changes - to as great an extent as is possible
				newmaster = this.master.apply_diff(diff, handler);
			}

			//Apply any local changes that haven't yet found their way across the network
			var locals = this.diff_queue.all();
			for (var i=0; i< locals.length; i++)
			{
				newmaster = Diff.stablePatch(locals[i], newmaster, new tinymce.concurrency.ErrorIgnorer());
			}

			//Set the current text to by this
			this.diff_history.set(newmaster);
			this.setContent(newmaster);
			this.selector.set(sel, newmaster);
		},

		/**
		 * When we are welcomed, set our user_id so that we know which diffs
		 * are from us, and which user is us on the user list.
		 *
		 * @param {string} id  The user_id we've been assigned
		 */
		on_welcome: function (id)
		{
			this.user_id = id;
		},

		/**
		 * When we recieve a sync, set the content to the new value - once we
		 * have applied all the diffs that would have been recieved before this was
		 * sent.
		 *
		 * @param {string} content  The new content of the document
		 */
		on_sync: function (content)
		{
			for (var i=0; i< this.presync_queue.length; i++)
			{
				content = Diff.stablePatch(this.presync_queue[i], content, new tinymce.concurrency.ErrorIgnorer());
			}
			this.master.set(content);
			this.diff_history.set(content);
			this.setContent(content);
			this.synced = true;
			this.presync_queue = null;
		},

		/**
		 * Someone has subscribed, let's send them a copy of the document.
		 */
		on_presync: function ()
		{
			if (this.synced)
			{
				this.connection.send({
					type: "sync",
					content: this.getContent()
				});
			}
			this.presync_queue = [];
		},

		/**
		 * Update the display of which users are also editing.
		 *
		 * @precondition  this.user_id must be set
		 * @param {Array<object>} users  The list of users
		 */
		on_users: function (users)
		{
			var others = []
			var nulls = 0;
			if (users.length == 1)
			{
				this.synced = true;
			}
			for (var i=0; i<users.length; i++)
			{
				if (users[i].id != this.user_id)
				{
					if (users[i].alias == "null")
						nulls += 1
					else
						others.push(users[i].alias);
				}
			}

			var msg;

			if (nulls > 0)
			{
				if (others.length > 0)
				{
					msg = " " + others.join(", ") + " and " + (nulls == 1 ? "one other" : nulls + " others") + " are editing at the moment";
				}
				else if(nulls > 1)
				{
					msg = " " + nulls + " others are editing at the moment.";
				}
				else
				{
					msg = " Someone else is editing at the moment.";
				}
			}
			else if (others.length > 0)
			{
				if (others.length > 1)
				{
					msg = " " + others.join(", ").replace(/,(?=[^,]*$)/," and") + " are also editing at the moment.";
				}
				else
				{
					msg = " " + others[0] + " is also editing at the moment.";
				}
			}
			else
			{
				msg = null;
			}

			this.notifier.inform(msg)
		},

		/**
		 * Get's a string representation of the current document.
		 *
		 * @return {string} The current document being edited.
		 */
		getContent: function ()
		{
			//undo changes made to ensure that we can have trailing spaces in firefox
			return this.ed.getContent().replace("\u2000<","<").replace("\u2000"," ");
		},

		/**
		 * Set's the string representation of the current document.
		 *
		 * @param {string} str  The content to use.
		 */
		setContent: function (str)
		{
			this.ed.setContent(str);
		},

		/**
		 * Find the difference between "now" and the last time a difference was taken
		 */
		takeDiff: function () {

			var content = this.getContent();

			var diff = this.diff_history.get(content);
			if(Diff.isNull(diff))
				return;
			this.diff_queue.append(diff);
			this.sendDiff(diff);

		},

		// DISABLED This causes way more problems that it's worth as TinyMCE will happily move style attributes around
		// making it almost impossible to remove the highlighting without changing the document.
//
//		preDiff: "<span class=\"tinymceDiff\" style=\"background-color: #FF9\">",
//		postDiff: "</span>",
//
//		colourDiff: function (diff) {
//			//TODO: handle the case the change is within a tag completely
//			for (var i=0; i<diff.length; i++)
//			{
//				for (var j=0; j<diff[i].c.length; j++)
//				{
//					var change = diff[i].c[j];
//
//					var open = change.i.indexOf('<');
//					var clos = change.i.indexOf('>');
//
//					if (open == -1 && clos == -1)
//						continue;
//
//					var res = '';
//
//					//We have a string something like '....<tag>'
//					if (open < clos)
//						res += this.preDiff;
//
//					res += change.i.replace(/(<[^>]*>)/g, this.postDiff+"$1"+this.preDiff); 
//
//					//We have a string that ends '...</tag>...'
//					if (change.i.lastIndexOf('<') < change.i.lastIndexOf('>'))
//						res += this.postDiff;
//					//we have a string that ends '...</ta'
//					else
//						res = res.replace(/(<[^>]*)$/,this.postDiff+"$1");
//
//					//We have a string that starts 'tag>...'
//					if (open > clos)
//						res = res.replace(/^([^<]*>)/, "$1" + this.preDiff);
//
//					change.i = res.replace(RegExp(this.preDiff + "(( |\s|&nbsp;)*)" + this.postDiff), "$1");
//				}
//			}
//			return diff;
//		},
//
//		removeColour: function () {
//			var node;
//			var raw_nodes = this.ed.dom.getRoot().getElementsByTagName('span');
//			var nodes = Array(raw_nodes.length); 
//
//			//raw_nodes is a NodeList, that mutates when the dom is changed.
//			//(in order to ensure each member is still a valid dom node)
//			//this is counter-intuitive and leads to wierd side-effects, so
//			//let's move the nodes into a normal array.
//			for (var i=0; i<raw_nodes.length; i++)
//				nodes[i] = raw_nodes[i];
//
//			for (var i=0; i < nodes.length; i++)
//			{
//				if (nodes[i].className.indexOf('tinymceDiff') >= 0)
//				{
//					while(nodes[i].childNodes.length)
//					{
//						nodes[i].parentNode.insertBefore(nodes[i].removeChild(nodes[i].firstChild),nodes[i]);
//					}
//					nodes[i].parentNode.removeChild(nodes[i]);
//				}
//			}
//		}

	});

/* *********** TinyMCE plugin code ********* */
	// Load plugin specific language pack
	//	tinymce.PluginManager.requireLangPack('concurrency');
	tinymce.create('tinymce.plugins.ConcurrencyPlugin', {
		/**
		 * Initializes the plugin, this will be executed after the plugin has been created.
		 * This call is done before the editor instance has finished it's initialization so use the onInit event
		 * of the editor instance to intercept that event.
		 *
		 * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
		 * @param {string} url Absolute URL to where the plugin is located.
		 */
		init : function(ed, url) {
			ed.onInit.add( function(ed) {
				new tinymce.concurrency.DiffSystem(ed);
			});
		},

		/**
		 * Creates control instances based in the incomming name. This method is normally not
		 * needed since the addButton method of the tinymce.Editor class is a more easy way of adding buttons
		 * but you sometimes need to create more complex controls like listboxes, split buttons etc then this
		 * method can be used to create those.
		 *
		 * @param {String} n Name of the control to create.
		 * @param {tinymce.ControlManager} cm Control manager to use inorder to create new control.
		 * @return {tinymce.ui.Control} New control instance or null if no control was created.
		 */
		createControl : function(n, cm) {
			return null;
		},

		/**
		 * Returns information about the plugin as a name/value array.
		 * The current keys are longname, author, authorurl, infourl and version.
		 *
		 * @return {Object} Name/value array containing information about the plugin.
		 */
		getInfo : function() {
			return {
				longname : 'Concurrency',
				author : 'Team Oscar',
				authorurl : '',
				infourl : '',
				version : "0.1"
			};
		}
	});

	// Register plugin
	tinymce.PluginManager.add('concurrency', tinymce.plugins.ConcurrencyPlugin);

})();
